# -*- coding: utf-8 -*-
"""
Created on Wed Oct  7 20:51:20 2020

@author: lenovo
"""

import os
import pandas as pd
import numpy as np
import sys
import random
from operator import itemgetter
from collections import defaultdict


'''
创建函数：
1、数据导入
2、数据清洗
ratingData: user_id, anime_id, rating, Favorable
'''
def dataCleaning(path):
    ratingData = pd.read_csv(path,encoding = "utf-8") #读取数据
    #去掉ratingData中rating列为-1的行（-1代表用户观看过但未评分）
    ratingData=ratingData[-ratingData.rating.isin([-1])]
    ratingData.drop_duplicates(keep="first",inplace=True)  #去重
    #将评分高于5的认定为用户喜欢该部动漫
    ratingData['Favorable'] = ratingData['rating'] > 5
    #随机选取20000行数据作为此次训练集和测试集的总数据
    ratingData = ratingData[ratingData['user_id'].isin(range(101))] 
    return ratingData

'''
创建函数：将数据划分成训练集和测试集
'''
def dataPartition(ratingData):
    userID = list(range(101))
    trainUserID = list(range(26))
    testUserID = list(set(userID) - set(trainUserID))
    trainData = ratingData[ratingData['user_id'].isin(trainUserID)]
    #为了避免某用户观看的历史动漫太多，所以在testUserID的数据中随机选取100行
    testData = ratingData[ratingData['user_id'].isin(testUserID)].sample(n=100,random_state=123,axis=0)
    return trainData, testData

'''
创建函数：提取特征
1、将原始数据ratingData分别按照用户ID和按照动漫ID进行分类
2、产生
'''
def getFeature(data):
    #在训练集中选取只包含喜欢电影的用户数据集
    favorable_ratings = data[data['Favorable']]
    #确定每个用户喜欢什么动漫，即按照用户ID进行分组，格式为：用户ID：｛电影ID集合}
    favorable_animes_by_users = dict((k, frozenset(v.values)) 
                                    for k,v in favorable_ratings.groupby('user_id')['anime_id'])
    #定义每部动漫的漫迷数
    num_favorable_by_anime = data[['anime_id','Favorable']].groupby('anime_id').sum()
    return favorable_animes_by_users, num_favorable_by_anime

'''
创建函数：生成所有的频繁项集（2-项集、3-项集直至无法收敛）
（删除1-项集，因为频繁1-项集不能生成关联规则）
'''
def allFrequentItems(favorable_animes_by_users, num_favorable_by_anime, min_support):
    frequent_itemsets = {}   #创建一个字典用于存放频繁项集
    #每一部电影作为一个项集，判断它是否够频繁(计算出现的频次)
    #这里frequent_itemsets[1]代表频繁1项集
    frequent_itemsets[1] = dict((frozenset((anime_id,)),row['Favorable'])
                            for anime_id, row in num_favorable_by_anime.iterrows() if row['Favorable'] > min_support)
    #存储运行中发现的新频繁项集
    for k in range(2,1000):
        cur_frequent_itemsets = find_frequent_itemsets(k,favorable_animes_by_users,frequent_itemsets[k-1],min_support)
        frequent_itemsets[k] = cur_frequent_itemsets
        if len(cur_frequent_itemsets) == 0:
            print("did not find any frequent itemsets of length {0}".format(k))
            #确保代码还在执行时，把缓冲区内容输出到终端，不易过多使用，flush操作（输出也是）所带来的计算会拖慢程序的运行速度
            sys.stdout.flush()
            break
        else:
            print("i found {0} frequent itemsets of length {1}".format(len(cur_frequent_itemsets),k))
            sys.stdout.flush()
    #删除频繁1-项集，因为频繁1-项集不能生成关联规则
    del frequent_itemsets[1]
    return frequent_itemsets

'''
创建函数：接受k-1项频繁项集，创建超集，检测频繁程度，生成k项频繁项集
'''
def find_frequent_itemsets(k,favorable_animes_by_users,k_1_itemsets,min_support):
    counts = defaultdict(int)
    for user,reviews in favorable_animes_by_users.items():
        for i in range(len(list(k_1_itemsets.keys()))):
            for j in range(i+1,len(list(k_1_itemsets.keys()))):
                f1 = list(k_1_itemsets.keys())[i]
                f2 = list(k_1_itemsets.keys())[j]
                if list(f1)[0:k-2] == list(f2)[0:k-2]:
                    k_itemsets = f1 | f2
                    if k_itemsets.issubset(reviews):
                        counts[k_itemsets] += 1
    return dict([(itemset, frequency) for itemset, frequency in counts.items() if frequency > min_support])
  
'''
创建函数：通过频繁项集生成强关联规则
输入：频繁项集，用户-动漫数据，最小置信度
输出：强关联规则（字典）
'''
def generateStrongRules(frequent_itemsets, favorable_animes_by_users,min_confidence):
    candidate_rules = []
    #遍历不同长度的频繁项集，为每个项集生成规则
    for itemset_length, itemset_counts in frequent_itemsets.items():
        #遍历每个项集
        for itemset in itemset_counts.keys():
            for conclusion in itemset:
                premise = itemset - set((conclusion,))
                candidate_rules.append((premise, conclusion))
            
    correct_counts = defaultdict(int)
    incorrect_counts = defaultdict(int)
    #遍历用户和打分数据，统计规则的次数，当规则的前提符合时，看一下用户是否喜欢结论中的电影
    for user, reviews in favorable_animes_by_users.items():
        for candidate_rule in candidate_rules:
            premise, conclusion = candidate_rule
            if premise.issubset(reviews):
                if conclusion in reviews:
                    correct_counts[candidate_rule] += 1
                else:
                    incorrect_counts[candidate_rule] += 1
    #计算规则的置信度
    rule_confidence = {candidate_rule:
                        correct_counts[candidate_rule]/(correct_counts[candidate_rule] + incorrect_counts[candidate_rule])
                        for candidate_rule in candidate_rules}
    #筛选出强规则
    deleteKeys = []
    for item in rule_confidence.items():
        key,value = item
        if(value < min_confidence): 
            deleteKeys.append(key)
    for key in deleteKeys:
        del rule_confidence[key]
    #强关联规则置信度降序
    StrongRules = sorted(rule_confidence.items(), key = itemgetter(1),reverse=True)
    return dict(StrongRules)

'''
创建函数：通过动漫ID获取动漫名称
'''
def getAnimeName(anime_id):
    animeData = pd.read_csv("archive/anime.csv",encoding="utf-8") #读取动漫信息数据
    anime_name_data = animeData.iloc[:,0:2]  #取出animeData中的动漫编号和动漫名称两列数据（前两列）
    name_object = anime_name_data[anime_name_data["anime_id"] == anime_id]['name']
    name = name_object.values[0]
    return name

'''创建函数：列出所有子集'''
def subSets(nums):
    res = [[]]
    for num in nums: 
        res += [ i + [num] for i in res]
    return res

'''
测试集进行测试---给测试用户推荐动漫
输入：测试集
输出：动漫推荐列表，以及相应的推荐值大小
'''
def recommendAnimes(testData, StrongRules):
    test_favorable_animes_by_users, test_num_favorable_by_anime = getFeature(testData)
    recommendationRuleDict = {}
    for user,animes in test_favorable_animes_by_users.items():
        recommendationRuleDict[user] = {}
        all_animesSets = subSets(animes)
        for item in all_animesSets:
            if(item != []):
                for rule in StrongRules:
                    premise,conclusion = rule
                    if(frozenset((conclusion,)).issubset(animes) == False):  #推荐列表中只包含用户没看过的动漫
                        if(frozenset(item) == premise):
                            recommendationRuleDict[user][rule] = StrongRules[rule]
    #将recommendationRuleDict中值为空的键值对删除，因为强规则有限，所以有些用户无法为其推荐动漫
    for k in list(recommendationRuleDict.keys()):
        if not recommendationRuleDict[k]:
            del recommendationRuleDict[k]
    No_recommendUser = set(test_favorable_animes_by_users.keys()) - set(recommendationRuleDict.keys()) #无法为其推荐动漫的用户ID
    #创建一个推荐动漫列表，字典的键--用户ID，值--{动漫ID：推荐值}
    recommendationAnimesList_ID = {}
    for user,recommendations in recommendationRuleDict.items():
        #将recommendations转成dataFrame形式
        df = pd.DataFrame.from_dict(recommendations,orient='index',columns = ['rate'])
        df = df.reset_index().rename(columns = {'index':'anime_id'})
        df['anime_id'] = df['anime_id'].apply(pd.Series).iloc[:,1]  #将id列的元组按照逗号隔开，并把规则的后项赋给id列
        df = df[['anime_id','rate']].groupby('anime_id').mean()  #按照id列分组，并将rate取其平均值
        df = df.reset_index().rename(columns = {'index':'anime_id'})  #重置index
        dict_recom = df.set_index('anime_id').T.to_dict('records') #将dataFrame转成dict形式
        dict_sorted = dict(sorted(dict_recom[0].items(), key = itemgetter(1),reverse=True))
        recommendationAnimesList_ID[user] = dict_sorted 
    #将recommendationAnimesList_ID中的动漫ID换成动漫名称
    recommendationAnimesList_Name = {}
    for item in recommendationAnimesList_ID.items():
        user, recommendAnimeID = item
        recommendationAnimesList_Name[user] = {}
        for anime_id in recommendAnimeID.keys():
            anime_name = getAnimeName(anime_id)
            recommendationAnimesList_Name[user][anime_name] = recommendationAnimesList_ID[user][anime_id]
    return No_recommendUser,recommendationRuleDict,recommendationAnimesList_ID,recommendationAnimesList_Name


def main():
    ratingData = dataCleaning("archive/rating.csv") #数据导入和处理
    trainData, testData = dataPartition(ratingData)
    min_support = 3
    min_confidence = 0.7
    favorable_animes_by_users, num_favorable_by_anime = getFeature(trainData)
    print('the process of generate Frequent Items:')
    frequent_itemsets = allFrequentItems(favorable_animes_by_users, num_favorable_by_anime, min_support)
    print('\n')
    StrongRules = generateStrongRules(frequent_itemsets, favorable_animes_by_users,min_confidence)
    No_recommendUser,recommendationRuleDict,recommendationAnimesList_ID,recommendationAnimesList_Name = recommendAnimes(testData, StrongRules)
    print('Unable to recommend userID of animation:',No_recommendUser)
    print('\n')
    print('The following is a list of recommended animations for users:')
    for user_id, recommends in recommendationAnimesList_Name.items():
        print('the animes recommendation list of user_id:',user_id)
        recommendDataFrame = pd.DataFrame.from_dict(recommends,orient='index',columns = ['probability'])
        recommendDataFrame = recommendDataFrame.reset_index().rename(columns = {'index':'anime_name'})
        print(recommendDataFrame)
        print('\n')
    
    
#执行main函数
if __name__ == '__main__':
    main()
    
    
    